/*******************************************************************************
 * SVN­Version­Information:
 * ­­­­­­­­­­­­­­­­­­­­­­­­---------------------
 * $LastChangedRevision: 6 $
 * $LastChangedBy: c.a.duevel $
 * $LastChangedDate: 2009-08-26 15:12:37 +0000 (Wed, 26 Aug 2009) $
 * $HeadURL: http://robofight.googlecode.com/svn/trunk/Server/src/de/hft_stuttgart/botwar/server/model/RunningGame.java $
 ******************************************************************************/
package de.hft_stuttgart.botwar.server.model;

import de.hft_stuttgart.botwar.common.commands.ChatCmd;
import de.hft_stuttgart.botwar.common.commands.Cmd;
import de.hft_stuttgart.botwar.common.commands.GameOverCmd;
import de.hft_stuttgart.botwar.common.commands.KickCmd;
import de.hft_stuttgart.botwar.common.commands.MoveCmd;
import de.hft_stuttgart.botwar.common.commands.StartGameCmd;
import de.hft_stuttgart.botwar.common.interfaces.IRemotePlayer.CHATTYPE;
import de.hft_stuttgart.botwar.common.models.Constants;
import de.hft_stuttgart.botwar.common.models.GameResultInfo;
import static de.hft_stuttgart.botwar.common.models.GameResultInfo.Result.*;
import de.hft_stuttgart.botwar.server.DataBase;
import de.hft_stuttgart.botwar.server.Main;
import de.hft_stuttgart.botwar.server.remote.Login;
import de.hft_stuttgart.botwar.server.remote.RemotePlayer;
import org.apache.log4j.Logger;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * This class represents a running game.
 * @author c.a.duevel@gmail.com, pmv-mail@gmx.de
 */
class RunningGame implements Runnable {

    private Map<Integer, Robot> robotMap;
    private Game game;
    private Logger logger = Logger.getLogger(RunningGame.class);
    private final int COUNT_PLAYER;
    private Thread thread;
    List<GameResultInfo> gameResult;
    Set<String> playingPlayer = new HashSet<String>();

    /**
     * Construct a running game for this game and starts it.
     * @param game The game to start.
     */
    public RunningGame(final Game game) {
        this.game = game;
        robotMap = game.getRobots();
        gameResult = new ArrayList(game.getPlayersStringList().size());
        playingPlayer.addAll(game.getPlayersStringList());
        COUNT_PLAYER = playingPlayer.size();

        thread = new Thread(this);
        thread.setPriority(Thread.MAX_PRIORITY);
        thread.start();
    }

    /**
     * Runs a game.
     */
    public void run() {
        for (RemotePlayer player : game.players) {
            player.addCommand(new StartGameCmd());
            player.getPlayer().startPlaying(); //say the db: the user is playing
        }
        long timer, loopTime;
        while (game.getNumConnectedPlayers() > 0) {
            timer = now();
            if (!runOneLoop()) {
                break;
            }
            loopTime = now() - timer;
            logger.info("Time for running one turn:" + loopTime + " ms");
            if (Constants.MS_PER_ROUND - loopTime > 0) {
                try {
                    Thread.sleep((long) (Constants.MS_PER_ROUND - loopTime));
                    logger.info("last LoopTime: " + (now() -
                            timer) + " ms");
                } catch (InterruptedException ex) {
                    logger.error(ex);
                }
            } else {
                logger.fatal("Server cannot keep up with the pace of the game:" + "last LoopTime: " + (now() - timer) + " ms");
                for (RemotePlayer player : game.getPlayers()) {
                    player.addCommand(new KickCmd(
                            Main.getSystemName(),
                            player.getID(),
                            "fatal error: overallocated"));
                    player.finishGame();
                }
                return;
            }
        }
    }

    private int countPlayer() {
        return COUNT_PLAYER;
    }

    /**
     * Returns whether the game is realy running, or not.
     * @return true if the game is running, otherwise false
     */
    public boolean isRunning() {
        return thread.isAlive();
    }

    /**
     * Checks for all players and removes them from the playingPlayer set
     * if they are dead.
     * @return Set with the name of all deadPlayers
     */
    Set<String> sortOutDeadPlayersNames() {
        Set<String> playersWithRobot = new HashSet<String>();
        for (Robot r : robotMap.values()) {
            playersWithRobot.add(r.getOWNER());
        }
        Set<String> deadPlayers = new HashSet<String>();
        Iterator<String> iter = playingPlayer.iterator();
        while (iter.hasNext()) {
            String playerName = iter.next();
            if (!playersWithRobot.contains(playerName)) {
                iter.remove();
                deadPlayers.add(playerName);
                logger.info("Player has lost: " + playerName);
            }
        }
        return deadPlayers;
    }

    /**
     *
     * @param deadPlayers Names of the player which where elimininated in the
     * last round
     * @return <code>true</code> if the game is over, else <code>false</code>
     */
    boolean checkIfGameIsOver(final Set<String> deadPlayers) {
        if (playingPlayer.size() == 1) {
            createGameResultsForLoosers(deadPlayers);
            //if there is only one player left he is the winner
            String winner = playingPlayer.iterator().next();
            gameResult.add(0, new GameResultInfo(winner, WON,
                    Coinsystem.getCoinsForGame(countPlayer(),
                    1)));
            logger.info(String.format("Gameresult for player %s is %s",
                    winner, WON.toString()));
            return true;
        } else if (playingPlayer.size() == 0) {
            assert deadPlayers.size() > 1;
            for (String player : deadPlayers) {
                gameResult.add(0, new GameResultInfo(player, DRAW,
                        Coinsystem.getCoinsForGame(
                        countPlayer(), 1, true)));
            }
            return true;
        } else {
            createGameResultsForLoosers(deadPlayers);
            return false;
        }

    }

    /**
     * clear up object references of the Game and GameInfo objects
     * send players the GameOverCmd
     * save statistics
     */
    void endGame() {
        GameOverCmd gameOverCmd = new GameOverCmd(gameResult);
        for (RemotePlayer player : game.getPlayers()) {
            player.addCommand(gameOverCmd);
            player.finishGame();
        }
        DataBase.get().updateStat(gameOverCmd);

        String infoText = String.format("Game %s has finished.", game.gameName);
        for (GameResultInfo resultInfo : gameResult) {
            switch (resultInfo.getResult()) {
                case WON:
                    infoText = String.format(
                            "Congratulations, \"%s\" is the winner of game \"%s\"!",
                            resultInfo.getPlayerId(),
                            game.gameName);
                    break;
                case DRAW:
                    infoText = String.format(
                            "How boring it all was, no winner in game \"%s\"!",
                            game.gameName);
                    break;
            }
        }

        for (RemotePlayer player : Login.loggedInPlayers()) {
            if (!player.getPlayer().isPlaying()) {
                player.addCommand(new ChatCmd(
                        CHATTYPE.LOBBY, Main.getSystemName(),
                        infoText));
            }
        }
        game.runningGame = null;
        game.close();
        game = null;
    }

    /**
     * This runs the Game for one step. And the end of the
     * step it checks weather this game has come to an end or not.
     * @return if the game is over returns <code>true</code>
     * else returns <code>false</code>
     */
    boolean runOneLoop() {
        List<Cmd> cmdList = new ArrayList<Cmd>();
        Cmd cmd;
        //update targets and move all robots
        double fractionOfRound = 1.0;
        for (Robot r : robotMap.values()) {
            r.updateTarget(robotMap.values());
        }
        while (fractionOfRound > 0) {
            double fractionToMove = fractionToMove(fractionOfRound);
            fractionOfRound -= fractionToMove;
            logger.debug("This round moving fraction of " + fractionToMove);
            for (Robot r : robotMap.values()) {
                r.move(fractionToMove);
            }
            if (fractionToMove <= 0.001) {//Actually it will be exactly 0.001
                //but it is not a good practice to compare exact floating
                //point values.
                checkForCollisions();
            }
        }
        for (Robot r : robotMap.values()) {
            cmdList.add(new MoveCmd(r.getPos(), r.getID()));
        }
        //as all robots have their new position - fire!
        for (Robot r : robotMap.values()) {
            cmd = r.fireAt(robotMap.values());
            if (cmd != null) {
                cmdList.add(cmd);
            }
        }
        //give the players all the commands
        for (RemotePlayer player : game.getPlayers()) {
            player.addCommandList(cmdList);
        }
        if (robotsDied()) {
            if (checkIfGameIsOver(sortOutDeadPlayersNames())) {
                endGame();
                return false;
            }
        }
        return true;
    }

    /**
     * Checks if there are dead robots and returns true if so.
     * Else returns false. Removes dead robots from the deadRobotList
     * @return <code>true</code> if there is a dead robot, else <code>false
     * </code>
     */
    boolean robotsDied() {
        boolean robotDied = false;
        Iterator<Robot> iter = robotMap.values().iterator();
        while (iter.hasNext()) {
            Robot r = iter.next();
            if (r.isDead()) {
                iter.remove();
                robotDied = true;
                logger.info(String.format("Robot %d died", r.getID()));
            }
        }
        return robotDied;

    }
    //prepares the gameResultInfo objects for these deadPlayers
    void createGameResultsForLoosers(final Collection<String> deadPlayers) {
        for (String player : deadPlayers) {
            gameResult.add(0, new GameResultInfo(player, LOST,
                    Coinsystem.getCoinsForGame(countPlayer(),
                    playingPlayer.size() + 1)));
        }
    }

    //just to make all those calls look nicer
    private static long now() {
        return System.currentTimeMillis();
    }

    double fractionToMove(final double fractionOfRoundLeft) {
        double fraction = fractionOfRoundLeft;
        for (Robot r : robotMap.values()) {
            if (r.getPos().equals(r.getTarget()) || r.getTarget() == null) {
                //if this is true the robot won't be moved
                continue;
            }
            for (Robot or : robotMap.values()) {
                if (r != or) {
                    fraction = Math.min(r.fractionOfRoundWithoutCrash(or),
                            fraction);
                    if (fraction <= 0.001) {
                        return 0.001;
                    }
                }
            }
        }
        assert fraction <= 1. : "Fraction must be between 0.001 and 1";
        assert fraction >= 0.001 : "Fraction must be between 0.001 and 1";
        return fraction;
    }

    //goes through all robots to look after collisions, if there are any
    //sets their target to their actual position
    void checkForCollisions() {
        for (Robot r : robotMap.values()) {
            for (Robot or : robotMap.values()) {
                if (r != or && r.collidesWith(or)) {
                    r.setTarget(r.getPos());
                    or.setTarget(or.getPos());
                }
            }
        }
    }
}
